/*
 *  CSE 40166, Computer Graphics, Fall 2013
 *  Assignment 4, Part 2
 *
 *  File: main.cpp
 *
 *  Kevin Sonnen
 *
 *  Description: Parses an OBJ File and Displays the 3D Model
 *  Stores the vertices and faces in data structures
 *  
 *  Usage: ./OBJ_Viewer [file1.obj] [file2.ppm] or [file1.ppm] [file2.obj]
 *
 */

// Include libraries
#include "OBJ_Parser.h"
#include "textureReader.h"

////////////////////////////////////////////////////////////
// GLOBAL VARIABLES
////////////////////////////////////////////////////////////

// Vector storing all of the objects we want to render
// NOTE: For this program, we will only render one object, so a vector is not necessary
// However, for future usage, using a vector is a good idea.
std::vector<Object> objectsToRender;

// Keep track of window width and height (in pixels)
// These may be changed when the window is resized
GLsizei windowWidth = 512, windowHeight = 512;

// Global array to store pixels for image output.
unsigned char * pixels;

// Variables for texture files
int texWidth, texHeight;
GLuint texHandle;

// Status of the mouse buttons
GLint leftMouseButton = GLUT_UP, rightMouseButton = GLUT_UP;
// Last known X and Y of the mouse
int mouseX = 0, mouseY = 0;

// Camera position in spherical coordinates
float cameraTheta, cameraPhi, cameraRadius;
// Camera position in cartesian coordinates
float cameraX, cameraY, cameraZ;

// Used to remember whether or not we've read in an OBJ and PPM file
bool haveObject = false;
bool haveTexture = false;

// Variables toggled using the keyboard
// Toggle whether or not the camera orbits the cradle.
bool cameraOrbitEnabled = false;
// Toggle whether or not to use a texture (if available)
bool enableTexturing = true;
// Toggle whether to use smooth or flat shading
bool smoothShading = false;
// Toggle whether to use perspective or orthographic camera
bool perspectiveCamera = false;
// Toggle whether or not to enable GL Lighting
bool lightingEnabled = true;
// Toggle whether the z or y axis is up
bool zUp = false;

//
// void generateImage()
//
// Capture the current screen, store in global pixels array and then write to file
// Called if the user presses 'p'
//
void generateImage() {
	static int frameno = 0;
	glReadPixels (0, 0, windowWidth, windowHeight, GL_RGB, GL_UNSIGNED_BYTE, pixels);
	char name[20];
	sprintf(name, "snapshot%d.ppm", frameno++);
	// Open file for writing
	FILE *fp = fopen( name, "w" );
	if (!fp) {
		perror( "fopen" );
		abort();
	}
	fprintf( fp, "P6\n%d %d\n255\n", windowWidth, windowHeight);
	// Write pixels to the file
	fwrite( pixels, windowWidth*windowHeight*3, sizeof(unsigned char), fp );
	// Close file
	fclose(fp);
}

//
// void updateCameraCartesian()
//
// Updates the camera's cartesian coordinates based on its spherical position
//
void updateCameraCartesian() {
	if (zUp) {
		cameraX = cameraRadius * sinf (cameraTheta) * sinf (cameraPhi);
		cameraY = cameraRadius * cosf (cameraTheta) * sinf (cameraPhi);
		cameraZ = cameraRadius * -cosf (cameraPhi);
	} else {
		cameraX = cameraRadius * sinf (cameraTheta) * sinf (cameraPhi);
		cameraZ = cameraRadius * -cosf (cameraTheta) * sinf (cameraPhi);
		cameraY = cameraRadius * -cosf (cameraPhi);
	}
}

//
// void cameraOrbit()
//
// Increments the camera's theta value. 
// When this is used in a loop, the camera orbits the cradle
//
void cameraOrbit() {
	// Orbit the camera in an amount proportional to the size of the object.
	// Otherwise, small objects spin very quickly. Large objects spin very slowly.
	cameraTheta+=.00005f/objectsToRender[0].diameter;
	updateCameraCartesian();
}

//
// void changeSize(GLsizei w, GLsizei h)
//
// Function to handle window resize. Will be registered as GLUT's rehape callback
// Aspect ratio of objects will remain the same regardless of the window size
void changeSize(int w, int h) {
	pixels = (unsigned char *)malloc(w*h*3*sizeof(unsigned char));
	if (!pixels) {
		perror("malloc");
		abort();
	}

    windowWidth = w;
    windowHeight = h;

    // Update the viewport
    glViewport(0, 0, w, h);

    // Update the projection matrix with the new window properties
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
	
    float aspectRatio = (float) w / (float)h;
    //gluPerspective(45.0,aspectRatio,0.1,100000);
    
    // Calculate camera view based on bounding box of object
    float objDiam = objectsToRender[0].diameter;
    GLfloat zNear = 10.0;
    GLfloat zFar = zNear + (objDiam*20);
    GLfloat left = -objDiam/2;
    GLfloat right = objDiam/2;
    GLfloat bottom = -objDiam/2;
    GLfloat top = objDiam/2;
    
    if (aspectRatio < 1.0) {
    	// Window is taller than it is wide
    	bottom /= aspectRatio;
    	top /= aspectRatio;
    } else {
    	left *= aspectRatio;
    	right *= aspectRatio;
    }
    // Use either perspective or orthographic camera
    if (perspectiveCamera) {
    	glFrustum(left, right, bottom, top, zNear, zFar);
    } else {
    	glOrtho(left, right, bottom, top, zNear, zFar);
    }
}

//
// void initScene()
//
// Basic OpenGL initialization function. Only needs to be called once.
//
void initScene() {
    glEnable(GL_DEPTH_TEST);

    // Enable a default light for the scene
    float lightCol[4] = { 1, 1, 1, 1};
    float ambientCol[4] = {0.0, 0.0, 0.0, 1.0};
    float lPosition[4] = { 10, 10, 10, 1 };
    
    glLightfv(GL_LIGHT0,GL_POSITION,lPosition);
    glLightfv(GL_LIGHT0,GL_DIFFUSE,lightCol);
    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientCol);
    
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);

    // Tell OpenGL not to use the material system, use whatever we pass with glColor()
    glEnable(GL_COLOR_MATERIAL);
    glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);

    // Set OpenGL's shading based on which option is enabled by default
    if (smoothShading)
	    glShadeModel(GL_SMOOTH);
    else
    	glShadeModel(GL_FLAT);
}

//
// void drawObjects()
//
// Iterate through the objectsToRender vector and draw them one face at a time
//
void drawObjects() {
	glColor4f(1.0, 1.0, 1.0, 1.0);	// Reset the color to white before drawing image
	if (haveTexture)
		glEnable( GL_TEXTURE_2D );
	for (unsigned int objNum=0; objNum<objectsToRender.size(); ++objNum) {
		for (unsigned int faceNum=0; faceNum<objectsToRender[objNum].faces.size(); ++faceNum) {
			glBegin(GL_POLYGON);
				bool useNormalsForVertex = objectsToRender[objNum].faces[faceNum].mapNormals();
				bool useTexturesForVertex = objectsToRender[objNum].faces[faceNum].mapTexture();
				// Loop over 3 vertices of face
				for(unsigned int vertNum=0; vertNum<3; ++vertNum) {
					if(useNormalsForVertex) {
						glNormal3fv(objectsToRender[objNum].getVertexNormals(faceNum, vertNum));
						//glNormal3fv(objectsToRender[objNum].normals[objectsToRender[objNum].faces[faceNum].getNormalIndices()[vertNum]].coord);
					}
					if(haveTexture && enableTexturing && useTexturesForVertex) {
						glTexCoord2fv(objectsToRender[objNum].getVertexTextureCoords(faceNum, vertNum));
					}
					glVertex3fv(objectsToRender[objNum].getVertexLocation(faceNum, vertNum));
					glColor4f(1.0, 1.0, 1.0, 1.0);
				}
			glEnd();
		}
	}
	glDisable(GL_TEXTURE_2D);
}

//
// void centerObject()
//
// Center the object so that the center of its bounding box is at the origin.
// Note: This may not make the object look centered because the bounding box
// only depends on the min and max vertices
//
void centerObject() {
	bounds objectBox = objectsToRender[0].boundingBox;	// Get the bounding box of the object
	
	float xT = -(objectBox.minX + (objectBox.maxX - objectBox.minX)/2);
	float yT = -(objectBox.minY + (objectBox.maxY - objectBox.minY)/2);
	float zT = -(objectBox.minZ + (objectBox.maxZ - objectBox.minZ)/2);
	glTranslatef(xT, yT, zT);
}

//
// void renderScene()
//
// Draw everything
//
void renderScene() {
    // Gpdate the modelview matrix based on the camera's position
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

	if (!zUp) {
		gluLookAt(cameraX, cameraY, cameraZ,	// Camera is located at (x,y,z)
					0, 0, 0,                    // Camera is looking at (0,0,0)
		            0.0f, 1.0f, 0.0f);			// Up vector is (0,1,0) (positive Z)
    } else {
    	gluLookAt(cameraX, cameraY, cameraZ,	// Camera is located at (x,y,z)
				0, 0, 0,                    // Camera is looking at (0,0,0)
                0.0f, 0.0f, 1.0f);			// Up vector is (1,0,0) (positive Z)
    }

    // Clear the render buffer
    glClearColor(0,0,0,1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
				
	// Translate the origin so the object will be centered, then draw it	
	glPushMatrix();
		centerObject();
		drawObjects();
	glPopMatrix();

    // Push the back buffer to the screen
    glutSwapBuffers();
}

//
// void keyboardPress (unsigned char key, int x, int y);
//
// GLUT callback to handle input from the keyboard
//
void keyboardPress(unsigned char key, int x, int y) {	
	switch( key ) {
		case 'q':				// Quit
		case 'Q':
		case 27:
			free(pixels); 		// Free allocated pixels memory
			exit(0);
			break;
		case 'p':				// Take a picture
		case 'P':
			generateImage();
			break;
		case 'o':				// Toggle orbiting camera
		case 'O':
			cameraOrbitEnabled = !cameraOrbitEnabled;
			break;
		case 't':				// Toggle Texturing
		case 'T':
			enableTexturing = !enableTexturing;
			break;
		case 's':				// Toggle smooth/flat shading
		case 'S':
			smoothShading = !smoothShading;
			if (smoothShading)
				glShadeModel(GL_SMOOTH);
			else
				glShadeModel(GL_FLAT);
			break;
		case 'c':				// Toggle perspective/orthographic camera
		case 'C':
			perspectiveCamera = !perspectiveCamera;
			changeSize(windowWidth, windowHeight);
			break;
		case 'l':				// Toggle GL Lighting
		case 'L':
			lightingEnabled = !lightingEnabled;
			if (lightingEnabled)
				glEnable( GL_LIGHTING);
			else
				glDisable( GL_LIGHTING );
			break;
		case 'z':				// Toggle whether the z or y axis is 'up'
		case 'Z':
			zUp = !zUp;
			updateCameraCartesian();
			break;
		default:
			break;
	}
}

//
// void mouseClick(int button, int state, int x, int y)
//
// Callback to handle mouse click events
//
void mouseClick(int button, int state, int x, int y) {
	// Update the left and right mouse button states, if applicable
    if(button == GLUT_LEFT_BUTTON) {
        leftMouseButton = state;
    } else if(button == GLUT_RIGHT_BUTTON) {
        rightMouseButton = state;
	}
    
    // Update the last seen X and Y coordinates of the mouse
    mouseX = x;
    mouseY = y;
}

//
// void mouseMotion(int x, int y)
//
// Callback to handle mouse movement events. Update cameraPhi, cameraTheta,
// 	and cameraRadius if the user is holding down a button
//
void mouseMotion(int x, int y)
{
	// If button is clicked while mouse is moving, rotate camera
    if(leftMouseButton == GLUT_DOWN || rightMouseButton == GLUT_DOWN ) {
		
		// Update theta and phi
		cameraTheta += (x - mouseX) * 0.005;
		cameraPhi   += (y - mouseY) * 0.005;
		
		// Keep Phi in (0, M_PI)
		if(cameraPhi <= 0)
			cameraPhi = 0+0.001;
		if(cameraPhi >= M_PI)
			cameraPhi = M_PI-0.001;
        
		// Update camera (x,y,z) based on (radius,theta,phi)
		updateCameraCartesian();
	}
	
    // Save the current mouseX and mouseY.
    mouseX = x;
    mouseY = y;
}

//
// void displayTimer(int duration)
//
// Timer function. Update the display
//
void displayTimer(int duration) {
	// Call the redisplay event
    glutPostRedisplay();
    
    // Orbit the camera if this option is enabled
    if (cameraOrbitEnabled) {
    	cameraOrbit();
    }

	// Schedule a timer to call this function recursively
    glutTimerFunc(duration, displayTimer, 0);
}

//
// int main(int argc, char* argv[])
//
int main( int argc, char* argv[] ) {
	// Filenames to be parsed are passed in as command line arguments
	// There should be one or two arguments passed
	if(argc < 2 || argc > 3) {
		std::cout << "Usage: " << argv[0] << " [file1.obj] [file2.ppm] or " << argv[0] << " [file1.ppm] [file2.obj]" << std::endl;
		exit(0);
	}
	
    // Create a double-buffered GLUT window at (50,50) with predefined windowsize
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(50,50);
    glutInitWindowSize(windowWidth,windowHeight);
    
    // Control how OpenGL stores pixels in memory for generating images
	glPixelStorei( GL_PACK_ROW_LENGTH, 0 );
	glPixelStorei( GL_PACK_SKIP_PIXELS, 0 );
	glPixelStorei( GL_PACK_ALIGNMENT, 1 );
		
    glutCreateWindow("OBJ Viewer");

    // Set the camera starting location
    cameraRadius = 13.0f;
    cameraTheta = 2.85;
    cameraPhi = 2.0;
	
	// Convert to cartesian coordinates
    updateCameraCartesian();
	
    // Register callbacks
    glutKeyboardFunc(keyboardPress);
    glutDisplayFunc(renderScene);
    glutReshapeFunc(changeSize);
	glutMouseFunc( mouseClick );
	glutMotionFunc( mouseMotion );

	// Iterate through the given files
	OBJ_Parser mParser;
	textureReader mTextureReader;
	for( int i=1; i<argc; ++i ) {
		std::string filename = argv[i];
		std::cout << "File " << i << ": " << filename << std::endl;
		// Get the file extension
		std::string fileExt = filename.substr(filename.find_last_of(".") + 1);
		// Transform the file extension to lowercase
		std::transform(fileExt.begin(), fileExt.end(), fileExt.begin(), ::tolower);
		// See if the extension is obj, ppm, or something else
		if (fileExt == "obj") {
			if (!haveObject) {
				if (mParser.parseOBJ(filename)) {
					std::cout << "Successfully read in model " << filename << std::endl;
					objectsToRender.push_back(mParser.getObject());
					objectsToRender[0].setDiameter();
					haveObject = true;
					// Set the window title to include the filename
					glutSetWindowTitle(("OBJ Viewer: " + filename).c_str());
				} else {
					std::cout << "Error: Could not read file " << filename << std::endl;
				}
			} else {
				std::cout << "Please provide only one OBJ file. The second will not be used.\n";
			}
		} else if (fileExt == "ppm") {
			if (!haveTexture) {
				// Read in texture image
				unsigned char *texData;
				if( mTextureReader.readPPM( filename, texWidth, texHeight, texData )) {
					std::cout << "Successfully read in image " << filename << std::endl;
					if( mTextureReader.registerOpenGLTexture( texData, texWidth, texHeight, texHandle ) ) {
						haveTexture = true;
					} else {
						std::cout << "Error: Could not register texture " << filename << std::endl;
					}
				} else {
					std::cout << "Error: Could not read image " << filename << std::endl;
				}
			} else {
				std::cout << "Please provide only one ppm file. The second will not be used.\n";
			}
		}
		else {
			std::cout << "Error: " << filename << " not an obj file, skipping file" << std::endl;
		}
		std::cout << std::endl;
	}

	if (haveObject) {	
		// OpenGL setup
		initScene();
		glutTimerFunc((unsigned int)(1000.0 / 60.0), displayTimer, (unsigned int)(1000.0 / 60.0));

		// Enter GLUT loop
		glutMainLoop();
    } else {
    	std::cout << "Please provide an OBJ file\n";
    }
    
	return 0;
}
